/**
 * Constructor of Table class
 * @param name (required) table name
 * @param records (option) array of records OR object of records, omit to create blank table
 * @param idField (option) fieldName of record's id-field, omit to using the member-index
 * @constructor
 */
var Table = function(name, records, idField){

    this.name = name;
    this._fields = {}; //field-id without [addRecord, delRecord]

    this._records = {};

    // Table Events : this._events.xxx : new Table.Event(['addRecord', 'delRecord'], function(table,id,record,eid,...args){})
    // Update Events : this._events.xxx : new Table.Event([fid3, fid7, ...], function(table,id,record,eid,...args){})
    this._events = {};

    this._subtables = {};

    this.messageToEvents = function(msg_msgs){
        let eids = new Set();
        let msgs = msg_msgs instanceof Array ? msg_msgs : [msg_msgs];
        for (let x in msgs){
            if (x !== undefined){
                let msg = msgs[x];
                for (let eid in this._events){
                    if (eid !== undefined && !eids.has(eid)){
                        if (this._events[eid].msgs.has(msg))
                            eids.add(eid);
                    }
                }
            }
        }
        return eids;
    };

    this.doEvents = function(msg_msgs, id, record, ...args){
        let eidSet = this.messageToEvents(msg_msgs);
        let eids = Array.from(eidSet);
        eids.sort((x, y) => x.tOrder - y.tOrder);
        for (let i = 0; i < eids.length; i++) {
            let eid = eids[i];
            if (this._events[eids].callback(this, id, record, eid, ...args) === false)
                return false;
        }
        return true;
    };

    this.existEvent = function(eid){
        return this._events[eid] !== undefined;
    };

    this.getEvent = function(eid){
        return this._events[eid];
    };

    this._addEvent = function(eid, event){
        if (event instanceof Table.Event){
            this._events[eid] = event;
            return true;
        }
        return false;
    };

    this.delEvent = function(eid) {
        if (!this.existEvent(eid))
            return true;
        delete this._events[eid];
        return true;
    };

    this.setEvent = function(eid, event){
        if (event === this._events[eid])
            return true;

        if (this.existEvent(eid) && !this.delEvent(eid))
            return false;

        return this._addEvent(eid, event);
    };

    this.gsetEvent = function(eid, event){
        if (event === undefined)
            return this._events[eid];
        if (event === null)
            return this.delEvent(eid);
        return this.setEvent(eid, event);
    };

    this.fixOfField = function(id, record, fid){
        return record === undefined || this._fields[fid](this, id, record, fid);
    };

    this.existField = function(fid){
        return this._fields[fid] !== undefined;
    };

    this.getField = function(fid){
        return this._fields[fid];
    };

    this._addField = function(fid, field){
        if (!field instanceof Table.Field)
            return false;

        this._fields[fid] = field;

        // fix exist records
        for (let id in this._records){
            if (id !== undefined){
                if (!this.fixOfField(id, this._records[id], fid))
                    this.del(id);
            }
        }

        // fix add/update record
        this.setEvent('fixField_' + fid, new Table.Event(['addRecord', fid], (table, _id, record, eid) => this.fixOfField(table, _id, record, fid)));
        return true;
    };

    this.delField = function(fid){
        if (!this.existField(fid))
            return true;

        delete this._fields[fid];
        return true;
    };

    this.delAllField = function(){
        for (let fid in this._fields){
            if (fid !== undefined)
                this.delField(fid);
        }
    };

    this.setField = function(fid, field){
        if (field === this._fields[fid])
            return true;

        if (this.existField(fid) && !this.delField(fid))
            return false;

        return this._addField(fid, field);
    };

    this.gsetField = function(fid, field){
        if (field === undefined)
            return this._fields[fid];
        if (field === null)
            return this.delSubtable(fid);
        return this.setSubtable(fid, field);
    };

    this.existSubtable = function(stid){
        return this._subtables[stid] !== undefined;
    };

    this.getSubtable = function(stid){
        return this._subtables[stid];
    };

    this._addSubtable = function(stid, subtable){
        if (subtable instanceof Table){
            this._subtables[stid] = subtable;
            subtable.setRecords(this._records);
            return true;
        }
        return false;
    };

    this.delSubtable = function(stid) {
        if (!this.existSubtable(stid))
            return true;
        delete this._subtables[stid];
        return true;
    };

    this.setSubtable = function(stid, subtable){
        if (subtable === this._subtables[stid])
            return true;

        if (this.existSubtable(stid) && !this.delSubtable(stid))
            return false;

        return this._addSubtable(stid, subtable);
    };

    this.gsetSubtable = function(stid, subtable){
        if (subtable === undefined)
            return this._subtables[stid];
        if (subtable === null)
            return this.delSubtable(stid);
        return this.setSubtable(stid, subtable);
    };

    this.exist = function(id){
        return this._records[id] !== undefined;
    };

    this.get = function(id){
        return this._records[id];
    };

    this._add = function(id, record){
        if (!this.doEvents('addRecord', id, record))
            return false;

        this._records[id] = record;

        for (let stid in this._subtables){
            if (stid !== undefined)
                this._subtables[stid]._add(id, record);
        }
        return true;
    };

    this.del = function(id){
        if (!this.exist(id))
            return true;

        if (!this.doEvents('delRecord', id, this._records[id]))
            return false;

        delete this._records[id];
        for (let stid in this._subtables){
            if (stid !== undefined)
                this._subtables[stid].del(id);
        }
        return true;
    };

    this.delAll = function(){
        for (let id in this._records)
            if (id !== undefined)
                this.del(id);
    };

    this.set = function(id, record){
        if (record === this._records[id])
            return true;

        if (this.exist(id) && !this.del(id))
            return false;

        return this._add(id, record);
    };

    this.gset = function(id, record){
        if (record === undefined)
            return this._records[id];
        if (record === null)
            return this.del(id);
        return this.set(id, record);
    };

    this.replaceId = function(id, newid){
        if (!this.set(newid, this._records[id]))
            return false;
        return this.del(id);
    };

    this.setRecords = function(records, idField){
        if (records instanceof Object){
            for (let i in records){
                if (i !== undefined){
                    let record = records[i];
                    let id = idField ? record[idField] : i;
                    if (id !== undefined) {
                        this.set(id, record);
                    }
                }
            }
        }
    };

    this._update = function(id, attrs_attrscb){
        var record = this._records[id];
        if (record == undefined)
            return false;

        let attrs = attrs_attrscb instanceof Function ? attrs_attrscb(this, id) : attrs_attrscb;

        let fkeys = [];
        for (let x in attrs){
            if (x !== undefined)
                fkeys.push(x);
        }
        if (!this.doEvents(fkeys, id, record, attrs))
            return false;

        for (let x in attrs){
            if (x !== undefined)
                record[x] = attrs[x];
        }
        return true;
    };

    this._defaultSelection = new Table.TableSelection(this);

    this.where = function(filter){
        return new Table.TableSelection(this, filter);
    };

    this.whereTogether = function(...args){ return this._defaultSelection.whereTogether(...args); };

    this.doWhile = function(cb){ this._defaultSelection.doWhile(cb); };

    this.update = function(id_attrs_attrscb, attrs_attrscb){
        if (typeof(id_attrs_attrscb) != 'object' && typeof(id_attrs_attrscb) != 'function' && typeof(id_attrs_attrscb) != 'undefined'){
            this._update(id_attrs_attrscb, attrs_attrscb);
        }else{
            this._defaultSelection.update(id_attrs_attrscb);
        }
    };

    this.count = function(){ return this._defaultSelection.count(); };

    this.first = function(){ return this._defaultSelection.first(); };

    this.randomOne = function(){ return this._defaultSelection.randomOne(); };

    this.randomSome = function(num){ return this._defaultSelection.randomSome(num); };

    this.any = function(cb){ return this._defaultSelection.any(cb); };

    this.all = function(cb){ return this._defaultSelection.all(cb); };

    this.sum = function(cb){ return this._defaultSelection.sum(cb); };

    this.product = function(cb){ return this._defaultSelection.product(cb); };

    this.min = function(cb){ return this._defaultSelection.min(cb); };

    this.max = function(cb){ return this._defaultSelection.max(cb); };

    this.concatString = function(cb, connector){ return this._defaultSelection.concatString(cb, connector); };

    this.concatArray = function(cb){ return this._defaultSelection.concatArray(cb); };

    this.extractTable = function(cb, name, idField){ return this._defaultSelection.extractTable(cb, name, idField); };

    if (typeof(records) === 'object')
        this.setRecords(records, idField);
};


/**
 * Constructor of Field class, see Table.setField().
 * @param fixCallback (option, default:() => true) field's fix-callback:(table, id, record, fieldId)=>{}, return false to avoid record
 * @constructor
 */
Table.Field = function(fixCallback){
    this.fixCallback = fixCallback instanceof Function ? fixCallback : () => true;
};


/**
 * Constructor of Event class, see Table.setEvent().
 * @param msg_msgs (required) message OR array of listen messages, 1: field-modify's message is fieldid, 2: table's message including: 'addRecord','delRecord'
 * @param callback (option, default:() => true) event's callback:(table, id, record, eventId)=>{}. return false to break events-loop
 * @param tOrder (option, default:0) event call time-order, in one message, all relative events are called one-by-one ordered by tOrder
 * @constructor
 */
Table.Event = function(msg_msgs, callback, tOrder){
    this.msgs = msg_msgs instanceof Array ? new Set(msg_msgs) : new Set([msg_msgs]);
    this.callback = callback instanceof Function ? callback : () => true;
    this.tOrder = tOrder instanceof Number ? tOrder : 0;
};


/**
 * A selection of table to doWhile many things, using by Table.where()
 * @param table (required) target table
 * @param filter (option, default:() => true), filter's callback: (table,id) => {}， return true when select this record
 * @constructor
 */
Table.TableSelection = function(table, filter){
    filter = filter instanceof Function ? filter : () => true;

    this._table = table;
    this._filter = filter;
    this._records = table._records;

    this.doWhile = function(cb){
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                if (!cb(this._table,id))
                    break;
            }
        }
    };

    this.where = function(filter){
        return new Table.TableSelection(this._table, (table, id) => this._filter(table, id) && filter(table, id));
    };

    this.whereTogether = function(...args){
        return new Table.ComboSelection(this, ...args);
    };

    this.update = function(attrs_attrscb){
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                this._table._update(id, attrs_attrscb);
            }
        }
    };

    this.count = function(){
        let c = 0;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
            }
        }
        return c;
    };

    this.first = function(){
        let c = 0;
        let v = {table:this._table,id:undefined};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v.id = id;
                break;
            }
        }
        return {cursor:c,val:v};
    };

    this.randomOne = function(){
        let count = this.count();
        let rand = Math.ceil(Math.random()*count);
        let c = 0;
        let v = {table:this._table,id:undefined};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (c == rand){
                    v.id = id;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.randomSome = function(num){
        let count = this.count();
        num = num <= count ? num : count;
        let rands = new Set();
        while (rands.size < num){
            let rand = Math.ceil(Math.random()*count);
            if (!rands.has(rand))
                rands.add(rand);
        }
        let c = 0;
        let v = [];
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (rands.has(c)){
                    v.push({table:this._table,id:id});
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.any = function(cb){
        let c = 0;
        let v = false;
        cb = cb ? cb : (id,table) => true;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (cb(this._table,id)){
                    v = true;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.all = function(cb){
        let c = 0;
        let v = true;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                if (!cb(this._table,id)){
                    v = false;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.sum = function(cb){
        let c = 0;
        let v = 0;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v += cb(this._table,id);
            }
        }
        return {cursor:c,val:v};
    };

    this.product = function(cb){
        let c = 0;
        let v = 1;
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v *= cb(this._table,id);
                if (v == 0)
                    break;
            }
        }
        return {cursor:c,val:v};
    };

    this.min = function(cb){
        let c = 0;
        let v = {table:this._table,id:undefined,minValue:Number.MAX_VALUE};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                let val = cb(this._table,id);
                if (v.minValue > val){
                    v.id = id;
                    v.minValue = val;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.max = function(cb){
        let c = 0;
        let v = {table:this._table,id:undefined,maxValue:Number.MIN_VALUE};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                let val = cb(this._table,id);
                if (v.maxValue < val){
                    v.id = id;
                    v.maxValue = val;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.concatString = function(cb, connector){
        connector = connector ? connector : '';
        let c = 0;
        let v = '';
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v = v + connector + cb(this._table,id);
            }
        }
        v = v.substr(connector.length);
        return {cursor:c,val:v};
    };

    this.concatArray = function(cb){
        let c = 0;
        let v = [];
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                c++;
                v.push(cb(this._table,id));
            }
        }
        return {cursor:c,val:v};
    };

    this.extractTable = function(cb, name, idField){
        let v = {};
        for (let id in this._records){
            if (id !== undefined && this._filter(this._table,id)){
                let rtn = cb(this._table,id);
                if (rtn !== undefined)
                    v[this._table.name + '_' + id] = rtn;
            }
        }
        return new Table(name, v, idField);
    };
};


Table.ComboSelection = function(...args) {

    this._selections = [];
    
    for (let x in args){
        let arg = args[x];
        if (arg instanceof Table){
            this._selections.push(arg._defaultSelection);
        }else if (arg instanceof Table.TableSelection){
            this._selections.push(arg);
        }else if (arg instanceof Table.ComboSelection){
            this._selections = this._selections.concat(arg._selections);
        }
    }

    this.doWhile = function(cb){
        for (let selid in this._selections){
            if (selid !== undefined){
                let selection = this._selections[selid];
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        if (!cb(selection._table,id))
                            return;
                    }
                }
            }
        }
    };

    this.where = function(filter){
        let selections = this._selections;
        for (let selid = 0; selid < selections.length; selid++)
            selections[selid] = selections[selid].where(filter);
        return new Table.ComboSelection(...selections);
    };

    this.whereTogether = function(...args){
        return new Table.ComboSelection(...this._selections, ...args);
    };

    this.update = function(attrs_attrscb){
        for (let selid in this._selections){
            if (selid !== undefined)
                this._selections[selid].update(attrs_attrscb);
        }
    };

    this.count = function(){
        let c = 0;
        for (let selid in this._selections){
            if (selid !== undefined)
                c += this._selections[selid].count();
        }
        return c;
    };

    this.first = function(){
        let c = 0;
        let v = {table:null,id:null};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].first();
                if (r.val != null && r.val.id != undefined){
                    c += r.cursor;
                    v = r.val;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.randomOne = function(){
        let count = this.count();
        let rand = Math.ceil(Math.random()*count);
        let c = 0;
        let v = {table:undefined,id:undefined};
        for (let selid in this._selections){
            if (selid !== undefined){
                let selection = this._selections[selid]
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        c++;
                        if (c == rand){
                            v.table = selection._table;
                            v.id = id;
                            break;
                        }
                    }
                }
            }
        }

        return {cursor:c,val:v};
    };

    this.randomSome = function(num){
        let count = this.count();
        num = num <= count ? num : count;
        let rands = new Set();
        while (rands.size < num){
            let rand = Math.ceil(Math.random()*count);
            if (!rands.has(rand))
                rands.add(rand);
        }

        let c = 0;
        let v = [];
        for (let selid in this._selections){
            if (selid !== undefined){
                var selection = this._selections[selid];
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        c++;
                        if (rands.has(c)){
                            v.push({table:selection._table,id:id});
                        }
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.any = function(cb){
        let c = 0;
        let v = false;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].any(cb);
                c += r.cursor;
                if (r.val === true){
                    v = r.val;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.all = function(cb){
        let c = 0;
        let v = true;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].all(cb);
                c += r.cursor;
                if (r.val === false){
                    v = false;
                    break;
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.sum = function(cb){
        let c = 0;
        let v = 0;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].sum(cb);
                c += r.cursor;
                v += r.val;
            }
        }
        return {cursor:c,val:v};
    };

    this.product = function(cb){
        let c = 0;
        let v = 1;
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].product(cb);
                c += r.cursor;
                v *= r.val;
                if (v == 0)
                    break;
            }
        }
        return {cursor:c,val:v};
    };

    this.min = function(cb){
        let c = 0;
        let v = {table:null,id:undefined,minValue:Number.MAX_VALUE};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].min(cb);
                c += r.cursor;
                if (r.val != null && r.val.id !== undefined){
                    if (v.minValue > r.minValue){
                        v.table = r.table;
                        v.id = r.id;
                        v.minValue = r.minValue;
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.max = function(cb){
        let c = 0;
        let v = {table:null,id:undefined,maxValue:Number.MIN_VALUE};
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].max(cb);
                c += r.cursor;
                if (r.val != null && r.val.id !== undefined){
                    if (v.maxValue < r.maxValue){
                        v.table = r.table;
                        v.id = r.id;
                        v.maxValue = r.maxValue;
                    }
                }
            }
        }
        return {cursor:c,val:v};
    };

    this.concatString = function(cb, connector){
        connector = connector ? connector : '';
        let c = 0;
        let v = '';
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].concatString(cb, connector);
                c += r.cursor;
                v = v + connector + r.val;
            }
        }
        v = v.substr(connector.length);
        return {cursor:c,val:v};
    };

    this.concatArray = function(cb){
        let c = 0;
        let v = [];
        for (let selid in this._selections){
            if (selid !== undefined){
                let r = this._selections[selid].concatArray(cb);
                c += r.cursor;
                v = v.concat(r.val);
            }
        }
        return {cursor:c,val:v};
    };

    this.extractTable = function(cb, name, idField){
        let v = {};
        for (let selid in this._selections){
            if (selid !== undefined){
                let selection = this._selections[selid]
                for (let id in selection._records){
                    if (id !== undefined && selection._filter(selection._table,id)){
                        let rtn = cb(selection._table,id);
                        if (rtn !== undefined)
                            v[selection._table.name + '_' + id] = rtn;
                    }
                }
            }
        }
        return table(name, v, idField);
    };
};

module.exports = Table;